package com.sprouting.ops.annotations.service;

import cn.hutool.core.util.StrUtil;
import com.sprouting.ops.model.ResultJson;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 适合单机的冥等性注解实现
 * 需要注意，会用用户登录的token
 *
 * @author ：LX（长沙麓谷）
 * @date 创建时间： 2023/3/14 9:19
 */
@Aspect
@Slf4j
@Component
public class IdempotentSingleAop {

    /**
     * token名称，防止token取其他的名字
     */
    @Value("${sa-token.token-name:token}")
    private String tokenName;


    /**
     * 线程安全的map，只适用于单机系统，这样也不用去考虑资源释放问题，多机版本的则不在这里使用
     */
    private static ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();

    /**
     * 切入点，拦截 controller
     * (..):最后这个星号表示方法名
     */
    @Pointcut(value = "@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController)")
    public void controllerPointCut() {
        /**
          @Pointcut(value = "execution(* com..controller.*.*(..))") 这2个拦截都可以，使用下面的则是根据注解来区分的，上面的则是根据路径来的
         */
    }

    /**
     * 必须 controller 以及 注解才会拦截
     */
    @Pointcut("@annotation(com.sprouting.ops.annotations.invoke.IdempotentSingle) && controllerPointCut()")
    public void IdempotentAspect(){

    }

    /**
     * 环绕通知
     * @param proceedingJoinPoint 切入点
     * @return
     */
    @Around("IdempotentAspect()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 1、--- 获取request
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;

        if (servletRequestAttributes != null){
            HttpServletRequest request = servletRequestAttributes.getRequest();
            // uri 只有后面的路径，url包含完整的请求路径。
            String uri = request.getRequestURI();
            // 加方法类型，用于兼容 result 风格
            String method = request.getMethod();
            // 获取token
            String token = request.getHeader(tokenName);
            if (StrUtil.isBlank(token)){
                // 再次尝试获取session
                HttpSession session = request.getSession();
                if (session != null){
                    // 将sessionId作为token
                    token = session.getId();
                    log.warn("[冥等性] 无token数据，将sessionId作为token");

                } else {
                    log.warn("[冥等性] 未拦截到token、也未获取到sessionId信息，请求 {} - {} 未进行冥等性验证，直接放行!", uri, method);
                    return proceedingJoinPoint.proceed();
                }
            }

            // 2、 判断释放在加锁中，否则加锁
            String key = token + method + uri;
            // 当key不存在的时候则返回null，当key存在的时候，则返回旧的值，比如之前存入了 2，这个时候返回的是2，不会返回现在输入的1
            String value = concurrentHashMap.putIfAbsent(key, "1");
            if (StrUtil.isNotBlank(value)){
                log.info("[冥等性] 拦截到接口冥等不通过，请求 {} - {} 被打回", method, uri);
                return ResultJson.repeatSubmission();
            }

            try {
                // 3、--- 调用业务代码
                Object proceed = proceedingJoinPoint.proceed();
                log.debug("[冥等性] 调用业务代码成功");

                // 一定要返回，否则返回数据丢失
                return proceed;

            } finally {
                // 4、--- 释放锁
                concurrentHashMap.remove(key);
            }

        } else {
            log.warn("[冥等性] 请求接口非web接口，未获取到web信息，不进行冥等性验证");
            return proceedingJoinPoint.proceed();
        }
    }

}
